using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;

namespace MirrorMirror
{
    public class ReflectionBuilder
    {
        private static readonly Dictionary<Type, OpCode> ValueTypesOpCodes;
        public const BindingFlags InstanceBindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
        private const string MethodPrefix = "<Mirror>_";

        static ReflectionBuilder()
        {
            ValueTypesOpCodes = new Dictionary<Type, OpCode>
                                    {
                                        {typeof (sbyte), OpCodes.Ldind_I1},
                                        {typeof (byte), OpCodes.Ldind_U1},
                                        {typeof (char), OpCodes.Ldind_U2},
                                        {typeof (short), OpCodes.Ldind_I2},
                                        {typeof (ushort), OpCodes.Ldind_U2},
                                        {typeof (int), OpCodes.Ldind_I4},
                                        {typeof (uint), OpCodes.Ldind_U4},
                                        {typeof (long), OpCodes.Ldind_I8},
                                        {typeof (ulong), OpCodes.Ldind_I8},
                                        {typeof (bool), OpCodes.Ldind_I1},
                                        {typeof (double), OpCodes.Ldind_R8},
                                        {typeof (float), OpCodes.Ldind_R4}
                                    };
        }

        private static Func<object, object,object> PropertySetter(Type type, PropertyInfo property)
        {
            return Invoker<Func<object, object, object>>(type, property.GetSetMethod(true));
        }

        private static MethodInfo GetMethod(Type type, string method, Type[] parametersTypes)
        {
            var info = GetMembersFromTypeInheritance(type, method)
                                .Cast<MethodInfo>()
                                .SingleOrDefault( x => x.GetParameters().Length == parametersTypes.Length 
                                                    && x.GetParameters().All(p => AreAlike(p.ParameterType, parametersTypes[p.Position])));

            if (info == null)
                throw new MemberNotFoundException(method);

            return info;
        }

        private static readonly Guid NullableGuid = typeof (Nullable<>).GUID;

        private static bool AreAlike(Type type, Type other)
        {
            if (type.IsAssignableFrom(other))
                return true;

            if(type.IsGenericType)
            {
                var generic = type.GetGenericTypeDefinition();
                if (generic.GUID == NullableGuid)
                    return AreAlike(Nullable.GetUnderlyingType(type), other);
            }

            return false;
        }

        private static Type[] ParameterTypesOf(MethodInfo method)
        {
            return method.GetParameters().Select(x => x.ParameterType).ToArray();
        }

        private static void EmitCall(ILGenerator il, MethodInfo method)
        {
            if (method.IsVirtual)
                il.Emit(OpCodes.Callvirt, method);
            else
                il.Emit(OpCodes.Call, method);
        }

        private static IEnumerable<MemberInfo> GetMembersFromTypeInheritance(Type type, string member)
        {
            if (type == null)
                return new MemberInfo[0];

            var infos = type.GetMember(member, InstanceBindingFlags);

            if(infos.Length > 0)
                return infos;

            return GetMembersFromTypeInheritance(type.BaseType, member);
        }

        private static MemberInfo GetFieldOrPropertyFromTypeInheritance(Type type, string member)
        {
            var infos = GetMembersFromTypeInheritance(type, member).ToArray();

            if (infos.Length > 1 && infos.Select(x=>x.DeclaringType).Distinct().Count() > 1)
                throw new InvalidPropertyOrFieldException("There are several members with the given name:" + member);

            if (infos.Length == 0)
                throw new MemberNotFoundException(string.Format("Member {0} not found.", member));

            return infos.First();
        }

        public Func<object, object> FieldOrPropertyGetter(Type type, string fieldOrProperty)
        {
            var member = GetFieldOrPropertyFromTypeInheritance(type, fieldOrProperty);

            switch (member.MemberType)
            {
                case MemberTypes.Field:
                    return FieldGetter(type, (FieldInfo)member);
                case MemberTypes.Property:
                    return PropertyGetter(type, (PropertyInfo)member);
                default:
                    throw new InvalidPropertyOrFieldException(string.Format("Member {0} found is not a property or field", member));
            }
        }
        public Func<object,object, object> FieldOrPropertySetter(Type type, string fieldOrProperty)
        {
            var member = GetFieldOrPropertyFromTypeInheritance(type, fieldOrProperty);

            switch (member.MemberType)
            {
                case MemberTypes.Field:
                    return FieldSetter(type, (FieldInfo)member);
                case MemberTypes.Property:
                    return PropertySetter(type, (PropertyInfo)member);
                default:
                    throw new InvalidPropertyOrFieldException(string.Format("Member {0} found is not a property or field", member));
            }
        }
        private static Func<object, object> FieldGetter(Type type, FieldInfo field)
        {
            var d = new DynamicMethod(MethodPrefix + "get<>" + field.Name, typeof(object), new[] { typeof(object) }, type, true);
            var il = d.GetILGenerator();

            il.Emit(OpCodes.Ldarg_0);

            il.Emit(OpCodes.Ldfld, field);

            if (field.FieldType.IsValueType)
                il.Emit(OpCodes.Box, field.FieldType);
            else
                il.Emit(OpCodes.Castclass, typeof(object));

            il.Emit(OpCodes.Ret);

            return (Func<object, object>)d.CreateDelegate(typeof(Func<object, object>));
        }


        private static Func<object, object, object> FieldSetter(Type type, FieldInfo field)
        {
            var d = new DynamicMethod(MethodPrefix + "set<>" + field.Name, typeof (object),
                                      new[] {typeof (object), typeof (object)}, type, true);

            var il = d.GetILGenerator();

            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldarg_1);

            var fieldType = field.FieldType;
            EmitConversionToObject(il, fieldType);

            il.Emit(OpCodes.Stfld, field);
            il.Emit(OpCodes.Ldnull);
            il.Emit(OpCodes.Ret);

            return (Func<object, object, object>) d.CreateDelegate(typeof (Func<object, object, object>));
        }

        private static void EmitConversionToObject(ILGenerator il, Type memberType)
        {
            if (memberType.IsValueType)
            {
                il.Emit(OpCodes.Unbox, memberType);

                if (ValueTypesOpCodes.ContainsKey(memberType))
                {
                    var load = ValueTypesOpCodes[memberType];
                    il.Emit(load);
                }
                else
                {
                    il.Emit(OpCodes.Ldobj, memberType);
                }
            }
            else
            {
                il.Emit(OpCodes.Castclass, memberType);
            }
        }

        private static Func<object, object> PropertyGetter(Type type, PropertyInfo property)
        {
            return Invoker<Func<object, object>>(type, property.GetGetMethod(true));
        }

        public TDelegate Invoker<TDelegate>(Type type, string method, Type[] parametersTypes)
        {
            return Invoker<TDelegate>(type, GetMethod(type,method,parametersTypes));
        }

        private static Type[] CreateObjectTypeArrayFrom<TDelegate>()
        {
            return 0.To(typeof(TDelegate).GetGenericArguments().Length)
                    .Select(x => typeof(object))
                    .ToArray();
        }

        private static TDelegate Invoker<TDelegate>(Type type, MethodInfo method)
        {
            var d = new DynamicMethod(MethodPrefix + method.Description(),
                                      typeof(object),
                                      CreateObjectTypeArrayFrom<TDelegate>(),
                                      type,
                                      true);

            var parametersTypes = ParameterTypesOf(method);
            
            var il = d.GetILGenerator();

            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Castclass, type);

            for (var i = 0; i < parametersTypes.Length; i++)
            {
                var parameterType = parametersTypes[i];
                il.Emit(OpCodes.Ldarg_S, i + 2);
                EmitConversionToObject(il, parameterType);
            }

            EmitCall(il, method);
            if (method.ReturnType != typeof(void))
            {
                if (method.ReturnType.IsValueType)
                    il.Emit(OpCodes.Box, method.ReturnType);
                else
                    il.Emit(OpCodes.Castclass, typeof(object));
            }
            else
            {
                il.Emit(OpCodes.Ldnull);
            }
            il.Emit(OpCodes.Ret);

            return (TDelegate)(object)d.CreateDelegate(typeof(TDelegate));
        }
    }
}